package editor;

import editor.code.LineField;
import editor.code.NumberField;
import editor.headers.Header;
import editor.widgets.ObservedIntComboBox;
import editor.widgets.ObservedStringComboBox;
import editor.widgets.ObservedStringTable;
import static editor.widgets.ObservedStringTable.*;
import event.ListAddEvent;
import event.ListMoveEvent;
import event.ListRemoveEvent;
import lang.Encoder;
import static lang.Encoder.*;
import model.Project;
import model.Segment;
import model.caret.Caret;
import model.caret.CaretRef;
import model.caret.TextCaret;
import model.code.Function;
import model.code.Rowable;
import model.code.line.LineList;
import model.code.line.SingleLine;
import model.symbol.Symbol;
import observer.ListObserver;
import observer.ObservedList;
import observer.ObservedString;
import util.IntList;
import widgets.CustomButton;
import widgets.ImageButton;

import javax.swing.*;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;

/**
 * This dialog box allows users to specify project options.
 *
 * @author Neil Dickson
 */
public class ProjectOptionsDialog extends JDialog implements ActionListener, ComponentListener, SingleLine.LineObserver {
	final private	Project			project;

	final private	Frame			rootParent;
//	private	JLabel			headerLabel;
	final private	JPanel			topPanel;
	final private	JPanel			buildSettingsPanel;
	final private	JPanel			outputTypePanel;
	final private	JRadioButton[]	outputTypeButtons;
	final private	Header.TextLabel	codeStartLabel;
	final private	SingleLine		entryPoint;
	final private	LineField		entryPointField;
	final private	ObservedStringTable<Project.ExternalInclude>	librariesField;
	final private	ObservedStringTable<Project.ExternalInclude>	importsField;
	final private	SegmentTable	table;
	final private	JPanel			bottomPanel;
//	private	CustomTabPane	tabs;
//	private	JPanel			segmentsPanel;
	final private	ImageButton[]	buttons;


	public static final String		UNINIT_DATA_TEXT		= "uninit data";
	public static final String		READ_ONLY_DATA_TEXT		= "read-only data";
	public static final String		ANY_CONTENT_TEXT		= "any content";
	public static final String		SEGMENT_ALIGN_TO		= "align to";
	public static final String		SEGMENT_START_AT		= "start at";
	public static final String[]	SEGMENT_ALIGN_OR_ADDR	= {SEGMENT_ALIGN_TO,SEGMENT_START_AT};
	public static final String[]	SEGMENT_TYPE_OPTIONS	= {ANY_CONTENT_TEXT,UNINIT_DATA_TEXT,READ_ONLY_DATA_TEXT};

	public ProjectOptionsDialog(Frame rootParent,Project project) {
		super(PwnIDE.editor,"Project Options",true);

		this.project = project;
		this.rootParent = rootParent;
		setLayout(new BorderLayout(0,0));

//		headerLabel = new JLabel("Project Options");
//		headerLabel.setFont(CustomDialog.HEADER_FONT);
//		FontMetrics metrics = getFontMetrics(CustomDialog.HEADER_FONT);
//		headerLabel.setSize(metrics.stringWidth(headerLabel.getText())+2,metrics.getHeight()+2);

		outputTypePanel = new JPanel(null);
		outputTypePanel.setLayout(new BoxLayout(outputTypePanel,BoxLayout.Y_AXIS));
		outputTypeButtons = new JRadioButton[NUM_OUTPUT_TYPES];
		int outputType = project.getOutputType().get();
		outputTypeButtons[OUTPUT_TYPE_WIN64_EXE] = new JRadioButton("64-bit Windows Application",outputType==OUTPUT_TYPE_WIN64_EXE);
		outputTypeButtons[OUTPUT_TYPE_WIN64_DLL] = new JRadioButton("64-bit Windows Library",outputType==OUTPUT_TYPE_WIN64_DLL);
		outputTypeButtons[OUTPUT_TYPE_WIN32_EXE] = new JRadioButton("32-bit Windows Application",outputType==OUTPUT_TYPE_WIN32_EXE);
		outputTypeButtons[OUTPUT_TYPE_WIN32_DLL] = new JRadioButton("32-bit Windows Library",outputType==OUTPUT_TYPE_WIN32_DLL);
		outputTypeButtons[OUTPUT_TYPE_OS_IMAGE] = new JRadioButton("Operating System Image",outputType==OUTPUT_TYPE_OS_IMAGE);

		ButtonGroup buttonGroup = new ButtonGroup();
		buttonGroup.add(outputTypeButtons[OUTPUT_TYPE_WIN64_EXE]);
		buttonGroup.add(outputTypeButtons[OUTPUT_TYPE_WIN64_DLL]);
		buttonGroup.add(outputTypeButtons[OUTPUT_TYPE_WIN32_EXE]);
		buttonGroup.add(outputTypeButtons[OUTPUT_TYPE_WIN32_DLL]);
		buttonGroup.add(outputTypeButtons[OUTPUT_TYPE_OS_IMAGE]);
		for (JRadioButton button : outputTypeButtons) {
			button.setBackground(Color.WHITE);
			button.addActionListener(this);
		}

		outputTypePanel.add(outputTypeButtons[OUTPUT_TYPE_WIN64_EXE]);
		outputTypePanel.add(outputTypeButtons[OUTPUT_TYPE_WIN64_DLL]);
		outputTypePanel.add(outputTypeButtons[OUTPUT_TYPE_WIN32_EXE]);
		outputTypePanel.add(outputTypeButtons[OUTPUT_TYPE_WIN32_DLL]);
		outputTypePanel.add(outputTypeButtons[OUTPUT_TYPE_OS_IMAGE]);

		outputTypePanel.setBackground(Color.WHITE);

		CaretRef ref = new CaretRef();

		buildSettingsPanel = new JPanel(null);
		buildSettingsPanel.setLayout(new BoxLayout(buildSettingsPanel,BoxLayout.Y_AXIS));
		codeStartLabel = new Header.TextLabel("Main Entry Point:",Header.TRANSPARENT_COLOUR);
		entryPoint = project.getEntryPointLine().copy();
		entryPoint.addReference(null,0,false);
		entryPoint.addObserver(this);
		valueChanged(entryPoint,entryPoint.toString());
		entryPointField = new LineField(entryPoint,ref,true);
		entryPointField.setFont(Header.OTHER_FONT);
		entryPointField.setForeground(Configuration.TEXT_COLOUR.getColour());
		entryPointField.setBackground(Header.TRANSPARENT_COLOUR);
		entryPointField.setOpaque(false);
		entryPointField.setBorder(Header.TEXT_FIELD_BORDER);
		entryPointField.addComponentListener(this);

		ObservedList<Project.ExternalInclude> libraries = project.getLibraryIncludes().copy();
		librariesField = new ObservedStringTable<Project.ExternalInclude>(ref,"Libraries:",libraries,Project.ExternalInclude.DUMMY);
		librariesField.setFont(Header.OTHER_FONT);
		librariesField.setForeground(Configuration.TEXT_COLOUR.getColour());
		librariesField.setBackground(Header.TRANSPARENT_COLOUR);
		librariesField.doLayout();
		librariesField.addComponentListener(this);

		ObservedList<Project.ExternalInclude> externalIncludes = project.getImportedIncludes().copy();
		importsField = new ObservedStringTable<Project.ExternalInclude>(ref,"External Includes:",externalIncludes,Project.ExternalInclude.DUMMY);
		importsField.setFont(Header.OTHER_FONT);
		importsField.setForeground(Configuration.TEXT_COLOUR.getColour());
		importsField.setBackground(Header.TRANSPARENT_COLOUR);
		importsField.doLayout();
		importsField.addComponentListener(this);

		buildSettingsPanel.add(codeStartLabel);
		buildSettingsPanel.add(entryPointField);
		buildSettingsPanel.add(librariesField);
		buildSettingsPanel.add(importsField);
		buildSettingsPanel.setBackground(Color.WHITE);

//		tabs = new CustomTabPane();
//		tabs.setBackground(BACKGROUND_COLOUR);
//		segmentsPanel = new JPanel(new BorderLayout());
//		segmentsPanel.setOpaque(false);
//		segmentsPanel.setBackground(TRANSPARENT);
//		JPanel defaultSegmentPanel = new JPanel();
//		JLabel label = new JLabel("Default Segments:");
//		label.setFont(FONT);
//		label.setBackground(TRANSPARENT);
//		defaultSegmentPanel.add(label);
//		defaultSegmentPanel.setOpaque(false);
//		defaultSegmentPanel.setBackground(TRANSPARENT);
////		ObservedIntComboBox nBitsBox = new ObservedIntComboBox(PwnIDE.editor.getProject().getDefaultSegmentNBits(), Segment.POSSIBLE_N_BITS);
////		ObservedIntComboBox alignmentBox = new ObservedIntComboBox(PwnIDE.editor.getProject().getDefaultSegmentAlignment(), Segment.POSSIBLE_ALIGNMENTS);
////		defaultSegmentPanel.add(nBitsBox);
////		defaultSegmentPanel.add(alignmentBox);
//		segmentsPanel.add(defaultSegmentPanel);
//		CustomScrollPane scroll = new CustomScrollPane(segmentsPanel);
//		tabs.addTab("Segments",scroll);
//		tabs.setSize(600,400);

		buttons = new ImageButton[2];
		buttons[0] = new ImageButton(Images.OK_UP,Images.OK_HOVER,Images.OK_DOWN,Images.OK_DISABLED);
		buttons[1] = new ImageButton(Images.CANCEL_UP,Images.CANCEL_HOVER,Images.CANCEL_DOWN,Images.CANCEL_DISABLED);
		buttons[0].addActionListener(this);
		buttons[1].addActionListener(this);
		JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER,16,6));
		buttonPanel.add(buttons[0]);
		buttonPanel.add(buttons[1]);
		buttonPanel.setBackground(Header.TRANSPARENT_COLOUR);

		ObservedList<Segment> segments = project.getSegments().copy();


//		add(headerLabel);
//		add(tabs);
		topPanel = new JPanel(null);
		topPanel.setLayout(new BoxLayout(topPanel,BoxLayout.X_AXIS));
		topPanel.add(outputTypePanel);
		topPanel.add(buildSettingsPanel);
		topPanel.setBackground(Color.WHITE);
		table = new SegmentTable("Segments:",segments,ref,outputType!=OUTPUT_TYPE_OS_IMAGE && outputType!=OUTPUT_TYPE_UNSPECIFIED);
		table.setBackground(Header.TRANSPARENT_COLOUR);
		table.doLayout();
		table.addComponentListener(this);
		bottomPanel = new JPanel(new BorderLayout());
		bottomPanel.add(table,BorderLayout.CENTER);
		bottomPanel.add(buttonPanel,BorderLayout.SOUTH);
		bottomPanel.setBackground(Color.WHITE);
		updateSize();
		add(topPanel,BorderLayout.CENTER);
		add(bottomPanel,BorderLayout.SOUTH);
//		add(outputTypePanel);
//		add(buttons[0]);
//		add(buttons[1]);
//		selected = NONE_SELECTED;
		invalidate();
		validate();
		requestFocus();
	}

	public void actionPerformed(ActionEvent e) {
		// If OK
		if (e.getSource()==buttons[0]) {
			setVisible(false);
			if (!project.getEntryPointLine().equals(entryPoint)) {
				project.getEntryPointLine().replaceAll(entryPoint.toString());
			}
			int outputType = Encoder.OUTPUT_TYPE_UNSPECIFIED;
			for (int i=0;i<outputTypeButtons.length;++i) {
				if (outputTypeButtons[i].isSelected()) {
					outputType = i;
					break;
				}
			}
			if (project.getOutputType().get()!=outputType) {
				project.getOutputType().set(outputType);
			}
			boolean needReopen = false;
			if (!librariesField.getElements().equals(project.getLibraryIncludes())) {
				// FIXME: Unload and load libraries not in both lists!!!
				project.getLibraryIncludes().clear();
				project.getLibraryIncludes().addAll(librariesField.getElements());
				needReopen = true;
			}
			if (!importsField.getElements().equals(project.getImportedIncludes())) {
				// FIXME: Unload and load libraries not in both lists!!!
				project.getImportedIncludes().clear();
				project.getImportedIncludes().addAll(importsField.getElements());
				needReopen = true;
			}
			if (!table.getElements().equals(project.getSegments())) {
				// Don't update the table as we remove elements or set them to null
				table.getElements().removeObserver(table);
				IntList indicesOfOriginals = new IntList(Math.min(project.getSegments().size(),table.getElements().size()));
				// Go through the original segments, looking for ones that are still here, but modified
				// Remove all original segments that no longer exist
				for (int i=0;i<project.getSegments().size();++i) {
					Segment segment = project.getSegments().get(i);
					String name = segment.getName().toString();
					boolean found = false;
					for (int j=0;j<table.getElements().size();++j) {
						if (table.getElements().get(j)!=null && name.equals(table.getOriginalNames().get(j))) {
							Segment newSeg = table.getElements().get(j);
							segment.getName().replaceAll(newSeg.getName().toString());
							segment.setType(newSeg.getType());
							segment.getAlignment().set(newSeg.getAlignment().get());
							segment.getAddress().set(newSeg.getAddress().get());
							segment.getFileAlignment().set(newSeg.getFileAlignment().get());
							segment.getFileAddress().set(newSeg.getFileAddress().get());
							if (segment.getnBits().get()!=newSeg.getnBits().get()) {
								segment.getnBits().set(newSeg.getnBits().get());
								// FIXME: Update the content of the segment here!!!
								needReopen = true;
							}
							indicesOfOriginals.add(j);
							table.getElements().set(j,null);
							found = true;
							break;
						}
					}
					if (!found) {
						project.getSegments().remove(i);
						// FIXME: Update the content of the segment here!!!
						needReopen = true;
						--i;
					}
				}
				// Sort the retained segments into their new order
				for (int i=1;i<project.getSegments().size();++i) {
					int j=0;
					while (j<i && indicesOfOriginals.get(j)<indicesOfOriginals.get(i)) {
						++j;
					}
					if (j!=i) {
						project.getSegments().move(i,j);
						indicesOfOriginals.add(j,indicesOfOriginals.removeElementAt(i));
					}
				}
				// Add any new segments, mixed in with the old segments, in order
				for (int i=0;i<table.getElements().size();++i) {
					if (table.getElements().get(i)!=null) {
						project.getSegments().add(i,table.getElements().get(i));
					}
				}
			}
			// Reopen the project to update the segment parsing or libraries/imports
			// TODO: This is a crummy way of updating the parsing.  Do it properly!
			if (needReopen) {
				PwnIDE.editor.openProject(project.getFile());
			}
		}
		// If Cancel
		else if (e.getSource()==buttons[1]) {
			setVisible(false);
		}
		else {
			int index=0;
			while (index<outputTypeButtons.length && e.getSource()!=outputTypeButtons[index]) {
				++index;
			}
			if (index>=outputTypeButtons.length) {
				return;
			}
			int nBits = -1;
			ObservedList<Segment> segments = null;
			boolean areSegmentsLocked = true;
			// NOTE: Do need to allow change even if same as current project output type so that people can change type back to what it was.
			if (index==OUTPUT_TYPE_WIN32_EXE || index==OUTPUT_TYPE_WIN32_DLL) {
				nBits = 32;
			}
			else if (index==OUTPUT_TYPE_WIN64_EXE || index==OUTPUT_TYPE_WIN64_DLL) {
				nBits = 64;
			}
			else if (index==OUTPUT_TYPE_OS_IMAGE) {
				areSegmentsLocked = false;
				segments = new ObservedList<Segment>(4);
				segments.add(new Segment("Code16",16,Segment.ANY_TYPE,Segment.DEFAULT_ALIGNMENT,0x7C00,0x200,Segment.DEFAULT_FILE_ALIGNMENT,0));
				segments.add(new Segment("UData0",16,Segment.UNINIT_DATA,Segment.DEFAULT_ALIGNMENT,0,0x400,Segment.DEFAULT_FILE_ALIGNMENT,Segment.ANY_ADDRESS));
				segments.add(new Segment("Code32",32,Segment.ANY_TYPE,Segment.DEFAULT_ALIGNMENT,0x600,Segment.NO_MAX_SIZE,Segment.DEFAULT_FILE_ALIGNMENT,0x200));
				segments.add(new Segment(Segment.DEFAULT_CODE_SEG_NAME,64,Segment.CODE,0x10,Segment.ANY_ADDRESS,Segment.NO_MAX_SIZE,0x10,Segment.ANY_ADDRESS));
				segments.add(new Segment(Segment.DEFAULT_READ_ONLY_DATA_SEG_NAME,64,Segment.READ_ONLY_DATA,0x10,Segment.ANY_ADDRESS,Segment.NO_MAX_SIZE,0x10,Segment.ANY_ADDRESS));
				segments.add(new Segment(Segment.DEFAULT_INIT_DATA_SEG_NAME,64,Segment.INIT_DATA,0x10,Segment.ANY_ADDRESS,Segment.NO_MAX_SIZE,0x10,Segment.ANY_ADDRESS));
				segments.add(new Segment("APCode16",16,Segment.ANY_TYPE,Segment.DEFAULT_ALIGNMENT,0x7000,Segment.NO_MAX_SIZE,Segment.DEFAULT_FILE_ALIGNMENT,0x6C00));
				segments.add(new Segment(Segment.DEFAULT_UNINIT_DATA_SEG_NAME,64,Segment.UNINIT_DATA,Segment.DEFAULT_ALIGNMENT,0x7E00,0x98200,Segment.DEFAULT_FILE_ALIGNMENT,Segment.ANY_ADDRESS));
			}
			if (nBits > 0) {
				segments = new ObservedList<Segment>(4);
				segments.add(new Segment(Segment.DEFAULT_CODE_SEG_NAME,nBits,Segment.CODE));
				// NOTE: The read-only data segment is put in between the code and the writable data to minimize cache flushing issues when writing to data.
				segments.add(new Segment(Segment.DEFAULT_READ_ONLY_DATA_SEG_NAME,nBits,Segment.READ_ONLY_DATA));
				segments.add(new Segment(Segment.DEFAULT_INIT_DATA_SEG_NAME,nBits,Segment.INIT_DATA));
				segments.add(new Segment(Segment.DEFAULT_UNINIT_DATA_SEG_NAME,nBits,Segment.UNINIT_DATA));
			}
			table.setSegments(segments,areSegmentsLocked);
		}
	}

	public void keyReleased(KeyEvent e) {
		if (e.getKeyCode()==KeyEvent.VK_ENTER) {
			actionPerformed(new ActionEvent(buttons[0],ActionEvent.ACTION_PERFORMED,""));
		}
		else if (e.getKeyCode()==KeyEvent.VK_ESCAPE) {
			actionPerformed(new ActionEvent(buttons[1],ActionEvent.ACTION_PERFORMED,""));
		}
	}

	private void updateSize() {
		int width = 300;
		int height;

		width = Math.max(width,outputTypePanel.getPreferredSize().width);
		height = outputTypePanel.getPreferredSize().height;

		int rightWidth = 0;
		int rightHeight = 0;
		for (int i=0;i<buildSettingsPanel.getComponentCount();++i) {
			Dimension dim = buildSettingsPanel.getComponent(i).getPreferredSize();
			rightWidth = Math.max(rightWidth,dim.width);
			rightHeight += dim.getHeight();
		}
		width += rightWidth;
		height = Math.max(height,rightHeight);

		int buttonsWidth = 0;
		int buttonsHeight = 0;
		for (ImageButton button : buttons) {
			buttonsWidth += button.getWidth();
			buttonsHeight = Math.max(buttonsHeight, button.getHeight());
		}
		buttonsWidth += (buttons.length-1)*16;	// Gaps between buttons
		buttonsHeight += 6*2;					// Vertical gap

		width = Math.max(width,buttonsWidth);
		height += buttonsHeight;

		width = Math.max(width,table.getWidth());
		height += table.getHeight();

		// Extra for the window border
		width += 16;
		height += 32;

		setSize(width,height);
		invalidate();
		validate();
		updatePopupLocationMiddle();
	}

	public void updatePopupLocationMiddle() {
		Point parentScreen = new Point(0,0);
		try {
			parentScreen = rootParent.getLocationOnScreen();
		}
		catch (Throwable t) {
			// Stuff gets thrown if the parent isn't visible yet
		}
		setLocation(new Point(((rootParent.getWidth()-getWidth())>>1)+parentScreen.x,((rootParent.getHeight()-getHeight())>>1) + parentScreen.y));
	}

	public void valueChanged(SingleLine line, String oldValue) {
		if (line==entryPoint) {
			String text = entryPoint.toString();
			Function newReference = null;
			ArrayList<Symbol> symbols = project.getScope().get(text);
			if (symbols!=null) {
				for (Symbol s : symbols) {
					if (s instanceof Function) {
						newReference = (Function)s;
						break;
					}
				}
			}
			entryPoint.getReferences()[0] = newReference;
		}
	}

	public void componentResized(ComponentEvent e) {
		updateSize();
	}
	public void componentMoved(ComponentEvent e) {}
	public void componentShown(ComponentEvent e) {}
	public void componentHidden(ComponentEvent e) {}

	/**
	 * This is a table for adding rows of {@link editor.widgets.ObservedStringField}s for editing {@link observer.ObservedString}s.
	 *
	 * It is useful for things with multiple columns, like function parameters, where each parameter has a name, type,
	 * and description, but it can also be used for things for which there is only one column, like author lists.
	 *
	 * @author Neil Dickson
	 */
	public class SegmentTable extends JComponent implements ListObserver<Segment>, ActionListener, AncestorListener {
		private	CaretRef						caretRef;
		private int								nRows;
		private final ArrayList<RowPanel>		rows;
		private	final ObservedList<Segment>		segments;
		private final ArrayList<String>			originalNames;
		private boolean							areSegmentsLocked;

		private final JPanel					titlePanel;
		private final JTextField				title;
		private final JTextField[]				columnLabels;
		private final CustomButton				add;

		private final SingleLine.LineObserver	cellObserver;

		/**
		 * Creates a {@link SegmentTable} from a list of {@link Segment}s
		 * @param title the table's title
		 * @param segments an {@link ObservedList} whose elements are the {@link Segment}s to be shown in each row
		 * @param caretRef the CaretRef controlling focus
		 */
		private SegmentTable(String title,ObservedList<Segment> segments,CaretRef caretRef,boolean areSegmentsLocked) {
			this.caretRef =  caretRef;
			this.areSegmentsLocked = areSegmentsLocked;
			titlePanel = new JPanel(null) {
				public Insets getInsets() {
					return new Insets(0,0,0,0);
				}
				public Insets getInsets(Insets insets) {
					insets.bottom = 0;
					insets.top = 0;
					insets.right = 0;
					insets.left = 0;
					return insets;
				}
			};
			if (title!=null && title.length()>0) {
				this.title = new Header.TextLabel(title,Header.TRANSPARENT_COLOUR);
				titlePanel.add(this.title);
			}
			else {
				this.title = null;
			}
			add = new ImageButton(ADD_ICONS[0],ADD_ICONS[1],ADD_ICONS[2],ADD_ICONS[3]);
			add.addActionListener(this);
			if (nRows==0) {
				titlePanel.add(add);
			}
			add(titlePanel);
			columnLabels = new JTextField[8];
			columnLabels[0] = new Header.TextLabel("Name",Header.TRANSPARENT_COLOUR);
			columnLabels[1] = new Header.TextLabel("Bits",Header.TRANSPARENT_COLOUR);
			columnLabels[2] = new Header.TextLabel("Type",Header.TRANSPARENT_COLOUR);
			columnLabels[3] = new Header.TextLabel("Location in Memory (hex)",Header.TRANSPARENT_COLOUR);
			columnLabels[5] = new Header.TextLabel("Location in File (hex)",Header.TRANSPARENT_COLOUR);
			columnLabels[7] = new Header.TextLabel("Max Size (hex)",Header.TRANSPARENT_COLOUR);
			for (int i=0;i<8;++i) {
				if (columnLabels[i]!=null) {
					columnLabels[i].setFont(Header.OTHER_FONT);
					add(columnLabels[i]);
				}
			}
			this.segments = segments;
			originalNames = new ArrayList<String>(segments.size());
			for (Segment segment : segments) {
				originalNames.add(segment.getName().toString());
			}
			nRows = segments.size();
			// Add this as a listener of the list
			segments.addObserver(this);
			// and observe the length of each cell too, for layout updating.
			cellObserver = new SingleLine.LineObserver() {
				public void valueChanged(SingleLine line, String oldValue) {
					if (line.length()!=oldValue.length()) {
						doLayout();
					}
				}
			};

			rows = new ArrayList<RowPanel>(nRows);
			for (int i=0;i<nRows;++i) {
				rows.add(new RowPanel(i));
				add(rows.get(i));
			}
			doLayout();
			addAncestorListener(this);
		}

		/**
		 * @return the list of {@link Rowable}s in this table
		 */
		public ObservedList<Segment> getElements() {
			return segments;
		}

		public ArrayList<String> getOriginalNames() {
			return originalNames;
		}

		/**
		 * @return the CaretRef of the file this is in, or if not in a file, the CaretRef given upon construction
		 */
		public CaretRef getCaretRef() {
			return caretRef;
		}

		/**
		 * Adds a row at the specified position with default values.
		 * @param row index of the row at which to insert the new row
		 * @param updateList true indicates that the list must be updated, i.e. that this widget caused the event
		 */
		public void addRow(int row,boolean updateList) {
			// Ensure that 0<=row<=nRows
			row = Math.min(nRows,Math.max(0,row));
			if (updateList) {
				// Add the new element
				segments.add(row,new Segment("",64,Segment.ANY_TYPE),this);
				PwnIDE.editor.eventDone(new ListAddEvent<Segment>(segments,row));
			}
			originalNames.add(row,null);
			// Add the new RowPanel at the correct position
			rows.add(row,new RowPanel(row));
			rows.get(row).setBackground(getBackground());
			rows.get(row).setForeground(getForeground());
			rows.get(row).setFont(getFont());
			add(rows.get(row),row+1);	// 1 extra due to title
			++nRows;
			// Update all rows to make sure row number set correctly and button states updated, including first on adding a second
			for (int i=0;i<nRows;++i) {
				rows.get(i).updateRow(i);
			}
			if (nRows==1) {
				titlePanel.remove(add);
			}
			doLayout();
			CaretRef caretRef = getCaretRef();
			caretRef.set(new TextCaret(caretRef,rows.get(row).name.getLines(),0,0,0));
			rows.get(row).name.requestFocus();
		}

		/**
		 * Removes a row at the specified
		 * @param row index of the row to remove
		 * @param updateList true indicates that the list must be updated, i.e. that this widget caused the event
		 */
		public void removeRow(int row,boolean updateList) {
			// Ensure that 0<=row<nRows
			if (row>=0 && row<nRows) {
				// First, remove the RowPanel
				remove(rows.get(row));
				if (updateList) {
					// Remove observers from each cell
					rows.get(row).removeLayoutObserver();
				}
				originalNames.remove(row);
				rows.remove(row);
				--nRows;
				if (updateList) {
					// Then remove the element
					Segment segment = segments.remove(row,this);
					Caret caret = getCaretRef().get();
					if (caret!=null && caret instanceof TextCaret) {
						LineList lines = ((TextCaret)caret).getLineList();
//						if (ArrayUtil.containsExact(segment.getStrings(),lines)) {
//							int newRow = (row<nRows) ? row : row-1;
//							if (nRows>0) {
//								((TextCaret)caret).set(rows.get(newRow).name.getLines(),0,0,0,0,0);
//								rows.get(newRow).name.requestFocus();
//							}
//						}
					}
					PwnIDE.editor.eventDone(new ListRemoveEvent<Segment>(segments,segment,row));
				}
				// Update all rows to make sure row number set correctly and button states updated, including first on removing a second
				for (int i=0;i<nRows;++i) {
					rows.get(i).updateRow(i);
				}
				if (nRows==0) {
					titlePanel.add(add);
				}
				doLayout();
			}
			else {
				ErrorHandler.showMessage("Sorry, for some unknown reason, the program is trying to remove a row with index "+row+" when there are "+nRows+" rows.  Hopefully this bug gets fixed in a later version.");
			}
		}

		/**
		 * Moves a row upward one position
		 * @param row index of the row to move upward
		 * @param updateList true indicates that the list must be updated, i.e. that this widget caused the event
		 */
		public void moveRowUp(int row,boolean updateList) {
			moveRow(row,row-1,updateList);
		}

		/**
		 * Moves a row downward one position
		 * @param row index of the row to move downward
		 * @param updateList true indicates that the list must be updated, i.e. that this widget caused the event
		 */
		public void moveRowDown(int row,boolean updateList) {
			moveRow(row,row+1,updateList);
		}

		/**
		 * Moves a row from one position to another
		 * @param from current index of the row to move
		 * @param to new index to which to move the row
		 * @param updateList true indicates that the list must be updated, i.e. that this widget caused the event
		 */
		protected void moveRow(int from,int to,boolean updateList) {
			if (from>=0 && from<nRows && to>=0 && to<nRows) {
				if (updateList) {
					// First, move the element
					segments.move(from,to,this);
					PwnIDE.editor.eventDone(new ListMoveEvent<Segment>(segments,from,to));
				}
				originalNames.add(to,originalNames.remove(from));
				// Then, move the RowPanel within the Container
				remove(rows.get(from));
				add(rows.get(from),to+1);	// 1 extra due to title
				rows.add(to,rows.remove(from));
				for (int i=Math.min(from,to);i<=Math.max(from,to);++i) {
					rows.get(i).updateRow(i);
				}
				doLayout();
			}
			else {
				ErrorHandler.showMessage("Sorry, for some unknown reason, the program is trying to move a row from index "+from+" to index "+to+" when there are "+nRows+" rows.  Hopefully this bug gets fixed in a later version.");
			}
		}

		public void actionPerformed(ActionEvent e) {
			if (e.getSource()==add) {
				try {
					addRow(0,true);
				}
				catch (Throwable t) {
					ErrorHandler.showException(t,"Sorry, an error occurred while adding/removing/moving a table row.\nIt should be easy to fix if you submit a bug report on http://forum.codecortex.com/\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
				}
			}
		}

		public void setBackground(Color bg) {
			super.setBackground(bg);
			setOpaque(bg.getAlpha()==255);
			if (title!=null) {
				title.setBackground(bg);
				title.setOpaque(bg.getAlpha()==255);
			}
			titlePanel.setBackground(bg);
			titlePanel.setOpaque(bg.getAlpha()==255);
			add.setBackground(bg);
			if (rows!=null) {
				for (RowPanel row : rows) {
					row.setBackground(bg);
				}
			}
		}

		public void setForeground(Color fg) {
			super.setForeground(fg);
			if (title!=null) {
				title.setForeground(fg);
			}
			titlePanel.setForeground(fg);
			add.setForeground(fg);
			if (rows!=null) {
				for (RowPanel row : rows) {
					row.setForeground(fg);
				}
			}
		}

		public void setFont(Font font) {
			super.setFont(font);
			if (rows!=null) {
				for (RowPanel row : rows) {
					row.setFont(font);
				}
				doLayout();
			}
		}

		public void doLayout() {
			int height = 0;
			int width = 0;
			if (title!=null) {
				FontMetrics font = title.getFontMetrics(title.getFont());
				title.setBounds(0,0,font.stringWidth(title.getText())+1,font.getHeight());	// 1 extra to avoid weird issue with selecting text in JTextField
				height = title.getHeight();
				width = title.getWidth()+4;
			}
			if (rows.size()>0) {
				add.setVisible(false);
			}
			else {
				add.setLocation(width,0);
				add.setVisible(true);
				height = Math.max(height,add.getHeight());
				width += add.getWidth();
			}
			titlePanel.setBounds(0,0,width,height);
			if (nRows>0) {
				FontMetrics font = rows.get(0).getComponent(0).getFontMetrics(rows.get(0).getComponent(0).getFont());
				int rowHeight = font.getHeight()+2;	// 2 extra for border thickness
				int x = 0;
				for (int col=0;col<rows.get(0).getComponentCount()-4;++col) {
					int maxWidth = 16;	// Minimum of 16 pixels
					for (RowPanel row : rows) {
						maxWidth = Math.max(maxWidth,row.getComponent(col).getPreferredSize().width+8+2);	// 8 for space, 2 extra for border thickness
					}
					for (RowPanel row : rows) {
						row.getComponent(col).setBounds(x,0,maxWidth,rowHeight);
					}
					if (columnLabels[col]!=null) {
						if (col+1<columnLabels.length && columnLabels[col+1]==null) {
							columnLabels[col].setLocation(x+maxWidth-(columnLabels[col].getFontMetrics(columnLabels[col].getFont()).stringWidth(columnLabels[col].getText())>>1),height);
						}
						else {
							columnLabels[col].setLocation(x+((maxWidth-columnLabels[col].getFontMetrics(columnLabels[col].getFont()).stringWidth(columnLabels[col].getText()))>>1),height);
						}
					}
					x += maxWidth+1;	// 1 extra for cell spacing
				}
				int addWidth = rows.get(0).add.getWidth()+1;		// 1 extra for spacing
				int removeWidth = rows.get(0).remove.getWidth()+1;	//
				int upWidth = rows.get(0).up.getWidth()+1;			//
				int downWidth = rows.get(0).down.getWidth();		// no extra, because at end
				for (RowPanel row : rows) {
					row.add.setLocation(x,0);
					row.remove.setLocation(x+addWidth,0);
					row.up.setLocation(x+addWidth+removeWidth,0);
					row.down.setLocation(x+addWidth+removeWidth+upWidth,0);
				}
				width = Math.max(width,x+addWidth+removeWidth+upWidth+downWidth);
				height += rowHeight;		// Extra for the column labels
				for (RowPanel row : rows) {
					row.setBounds(0,height,width,rowHeight);
					height += rowHeight+1;	// 1 extra for cell spacing
				}
				--height;					// subtract the final cell spacing, since at end
			}
			Dimension dim = new Dimension(width,height);
			setSize(dim);
			setMinimumSize(dim);
			setPreferredSize(dim);
			// For some reason, providing Short.MAX_VALUE indicates that it can be any width, but left-aligns it in a managed layout... *sigh*
			setMaximumSize(new Dimension(Short.MAX_VALUE,height));
		}

		public void listElementAdded(ObservedList<Segment> s, int index) {
			addRow(index,false);
		}

		public void listElementRemoved(ObservedList<Segment> s, int index, Segment element) {
			// Remove observers from each cell
			rows.get(index).removeLayoutObserver();
			removeRow(index,false);
		}

		public void listElementSet(ObservedList<Segment> s, int index, Segment oldElement) {
			ErrorHandler.showNotImplementedMessage("Sorry, there's an unimplemented function named ObservedStringTable.listElementSet, and it was called, but it shouldn't have been.  Hopefully this bug gets fixed in a later version.");
		}

		public void listElementMoved(ObservedList<Segment> s, int from, int to) {
			moveRow(from,to,false);
		}

		public void ancestorAdded(AncestorEvent event) {
			segments.addObserver(this);
			for (RowPanel row : rows) {
				row.addLayoutObserver();
			}
		}

		public void ancestorRemoved(AncestorEvent event) {
			segments.removeObserver(this);
			for (RowPanel row : rows) {
				row.removeLayoutObserver();
			}
		}

		public void ancestorMoved(AncestorEvent event) {}

		public void setSegments(ObservedList<Segment> segments, boolean areSegmentsLocked) {
			this.areSegmentsLocked = areSegmentsLocked;
			this.segments.clear();
			this.segments.addAll(segments);
		}

		public class RowPanel extends JPanel implements ActionListener, ItemListener {
			int								row;
			final Segment					segment;
			final LineField					name;
			final ObservedIntComboBox 		nBits;
			final ObservedStringComboBox 	type;
			final ObservedStringComboBox	memAlignOrAddr;
			final NumberField				memLocation;
			final ObservedStringComboBox	fileAlignOrAddr;
			final NumberField				fileLocation;
			final NumberField				maxSize;
			final CustomButton				add;
			final CustomButton				remove;
			final CustomButton				up;
			final CustomButton				down;

			RowPanel(int row) {
				super(null);
				segment = segments.get(row);
				this.row = row;
				name = new LineField(segment.getName(),getCaretRef(),true);
				name.setBorder(TEXT_FIELD_BORDER);
				name.setEnabled(!areSegmentsLocked);
				add(name);
				nBits = new ObservedIntComboBox(segment.getnBits(),Segment.POSSIBLE_N_BITS);
				nBits.setEnabled(!areSegmentsLocked);
				add(nBits);
				ObservedString typeString;
				boolean isDefault = false;
				if (segment.getType()==Segment.CODE) {
					typeString = new ObservedString("default code");
					isDefault = true;
				}
				else if (segment.getType()==Segment.INIT_DATA) {
					typeString = new ObservedString("default data");
					isDefault = true;
				}
				else if (segment.getType()==Segment.READ_ONLY_DATA) {
					typeString = new ObservedString(READ_ONLY_DATA_TEXT);
				}
				else if (segment.getType()==Segment.UNINIT_DATA) {
					typeString = new ObservedString(UNINIT_DATA_TEXT);
				}
				else { // segment.getType()==Segment.ANY_TYPE
					typeString = new ObservedString(ANY_CONTENT_TEXT);
				}
				type = new ObservedStringComboBox(typeString,isDefault ? new String[]{typeString.toString()} : SEGMENT_TYPE_OPTIONS);
				type.setEnabled(!isDefault && !areSegmentsLocked);
				type.addItemListener(this);
				add(type);
				memAlignOrAddr = new ObservedStringComboBox(new ObservedString((segment.getAddress().get()!=Segment.ANY_ADDRESS) ? SEGMENT_START_AT : SEGMENT_ALIGN_TO), SEGMENT_ALIGN_OR_ADDR);
				memAlignOrAddr.addItemListener(this);
				memAlignOrAddr.setEnabled(!areSegmentsLocked);
				add(memAlignOrAddr);
				memLocation = new NumberField((segment.getAddress().get()!=Segment.ANY_ADDRESS) ? segment.getAddress() : segment.getAlignment(),16,(segment.getFileAddress().get()!=Segment.ANY_ADDRESS) ? Segment.ANY_ADDRESS : Segment.DEFAULT_ALIGNMENT,getCaretRef());
				memLocation.setBorder(TEXT_FIELD_BORDER);
				memLocation.setEnabled(!areSegmentsLocked);
				add(memLocation);
				fileAlignOrAddr = new ObservedStringComboBox(new ObservedString((segment.getFileAddress().get()!=Segment.ANY_ADDRESS) ? SEGMENT_START_AT : SEGMENT_ALIGN_TO), SEGMENT_ALIGN_OR_ADDR);
				fileAlignOrAddr.addItemListener(this);
				fileAlignOrAddr.setEnabled(!areSegmentsLocked);
				add(fileAlignOrAddr);
				fileAlignOrAddr.setVisible(segment.getType()!=Segment.UNINIT_DATA);
				fileLocation = new NumberField((segment.getFileAddress().get()!=Segment.ANY_ADDRESS) ? segment.getFileAddress() : segment.getFileAlignment(),16,(segment.getFileAddress().get()!=Segment.ANY_ADDRESS) ? Segment.ANY_ADDRESS : Segment.DEFAULT_FILE_ALIGNMENT,getCaretRef());
				fileLocation.setBorder(TEXT_FIELD_BORDER);
				fileLocation.setEnabled(!areSegmentsLocked);
				add(fileLocation);
				fileLocation.setVisible(segment.getType()!=Segment.UNINIT_DATA);
				maxSize = new NumberField(segment.getMaxSize(),16,Segment.NO_MAX_SIZE,getCaretRef());
				maxSize.setBorder(TEXT_FIELD_BORDER);
				maxSize.setEnabled(!areSegmentsLocked);
				add(maxSize);
				add = new ImageButton(ADD_ICONS[0],ADD_ICONS[1],ADD_ICONS[2],ADD_ICONS[3],!areSegmentsLocked);
				add.addActionListener(this);
				add(add);
				remove = new ImageButton(REMOVE_ICONS[0],REMOVE_ICONS[1],REMOVE_ICONS[2],REMOVE_ICONS[3],!isDefault && !areSegmentsLocked);
				remove.addActionListener(this);
				add(remove);
				up = new ImageButton(UP_ICONS[0],UP_ICONS[1],UP_ICONS[2],UP_ICONS[3],row!=0 && !areSegmentsLocked);
				up.addActionListener(this);
				add(up);
				down = new ImageButton(DOWN_ICONS[0],DOWN_ICONS[1],DOWN_ICONS[2],DOWN_ICONS[3],row!=nRows-1 && !areSegmentsLocked);
				down.addActionListener(this);
				add(down);
				// Add observers on each cell to check for size changes
				addLayoutObserver();
			}

			public void actionPerformed(ActionEvent e) {
				try {
					if (e.getSource()==add) {
						addRow(row+1,true);
					}
					else if (e.getSource()==remove) {
						if (segment.getType()!=Segment.CODE && segment.getType()!=Segment.INIT_DATA) {
							removeRow(row,true);
						}
					}
					else if (e.getSource()==up) {
						moveRowUp(row,true);
					}
					else if (e.getSource()==down) {
						moveRowDown(row,true);
					}
				}
				catch (Throwable t) {
					ErrorHandler.showException(t,"Sorry, an error occurred while adding/removing/moving a table row.\nIt should be easy to fix if you submit a bug report on http://forum.codecortex.com/\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
				}
			}
			public void updateRow(int newRow) {
				row = newRow;
				up.setEnabled(row!=0 && !areSegmentsLocked);
				down.setEnabled(row!=nRows-1 && !areSegmentsLocked);
			}

			public void setBackground(Color bg) {
				super.setBackground(bg);
				setOpaque(bg.getAlpha()==255);
				for (int i=0;i<getComponentCount();++i) {
					getComponent(i).setBackground(bg);
				}
			}

			public void setForeground(Color fg) {
				super.setForeground(fg);
				for (int i=0;i<getComponentCount();++i) {
					getComponent(i).setForeground(fg);
				}
			}

			public void setFont(Font font) {
				super.setFont(font);
				for (int i=0;i<getComponentCount();++i) {
					getComponent(i).setFont(font);
				}
				// NOTE: Do not redo the layout here to avoid redoing the layout for every row.
			}

			public void addLayoutObserver() {
				((SingleLine)name.getLines().get(0)).addObserver(cellObserver);
				((SingleLine)memLocation.getLines().get(0)).addObserver(cellObserver);
				((SingleLine)fileLocation.getLines().get(0)).addObserver(cellObserver);
				((SingleLine)maxSize.getLines().get(0)).addObserver(cellObserver);
			}

			public void removeLayoutObserver() {
				((SingleLine)name.getLines().get(0)).removeObserver(cellObserver);
				((SingleLine)memLocation.getLines().get(0)).removeObserver(cellObserver);
				((SingleLine)fileLocation.getLines().get(0)).removeObserver(cellObserver);
				((SingleLine)maxSize.getLines().get(0)).removeObserver(cellObserver);
			}

			public void itemStateChanged(ItemEvent e) {
				if (e.getSource()==type) {
					fileAlignOrAddr.setVisible(type.getSelectedItem()!=UNINIT_DATA_TEXT);
					fileLocation.setVisible(type.getSelectedItem()!=UNINIT_DATA_TEXT);
				}
				else if (e.getSource()==memAlignOrAddr) {
					if (memAlignOrAddr.getSelectedItem()==SEGMENT_START_AT) {
						segment.getAlignment().set(Segment.DEFAULT_ALIGNMENT);
						memLocation.setObservedLong(segment.getAddress(),Segment.ANY_ADDRESS);
					}
					else {
						segment.getAddress().set(Segment.ANY_ADDRESS);
						memLocation.setObservedLong(segment.getAlignment(),Segment.DEFAULT_ALIGNMENT);
					}
				}
				else if (e.getSource()==fileAlignOrAddr) {
					if (fileAlignOrAddr.getSelectedItem()==SEGMENT_START_AT) {
						segment.getFileAlignment().set(Segment.DEFAULT_FILE_ALIGNMENT);
						fileLocation.setObservedLong(segment.getFileAddress(),Segment.ANY_ADDRESS);
					}
					else {
						segment.getFileAddress().set(Segment.ANY_ADDRESS);
						fileLocation.setObservedLong(segment.getFileAlignment(),Segment.DEFAULT_FILE_ALIGNMENT);
					}
				}
			}
		}
	}

}
